package im.composer.media.sound.codec;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.spi.AudioFileReader;

import org.tritonus.share.sampled.AudioFileTypes;
import org.tritonus.share.sampled.FloatInputStream;

import im.composer.media.audio.stream.AudioByteBuffer;
import im.composer.media.sound.codec.seek.LameSeekSupport;
import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.SampleBuffer;

public class LameFileReader extends AudioFileReader implements PathAwareAudioFileReader {

	private static final ByteOrder BYTEORDER = ByteOrder.BIG_ENDIAN;

	private AudioFileFormat getAudioFileFormat(Bitstream bitstream, long len, LameSeekSupport seek_support) throws UnsupportedAudioFileException, IOException {
		Header h;
		try {
			h = bitstream.readFrame();
		} catch (BitstreamException e) {
			throw new UnsupportedAudioFileException(e.getLocalizedMessage());
		}
		if (h == null) {
			throw new UnsupportedAudioFileException("NOT valid MP3 stream!");
		}
		int channels = h.mode() == Header.SINGLE_CHANNEL ? 1 : 2;
		AudioFormat format = new AudioFormat(h.frequency(), 16, channels, true, BYTEORDER == ByteOrder.BIG_ENDIAN);
		int frameLength = (int) (h.total_ms(len) / 1000f * h.frequency());
		if (seek_support != null && seek_support.isEnabled()) {
			long l = seek_support.maxIndex();
			if (l > 0) {
				frameLength = (int) l;
			}
		}
		return new AudioFileFormat(new AudioFileTypes("LAME", "mp3"), format, frameLength);
	}

	@Override
	public AudioFileFormat getAudioFileFormat(InputStream stream) throws UnsupportedAudioFileException, IOException {
		return getAudioFileFormat(new Bitstream(stream), -1, null);
	}

	@Override
	public AudioFileFormat getAudioFileFormat(URL url) throws UnsupportedAudioFileException, IOException {
		URLConnection conn = url.openConnection();
		if (!conn.getContentType().equals("audio/mpeg")) {
			throw new UnsupportedAudioFileException();
		}
		long len = conn.getContentLengthLong();
		InputStream in = url.openStream();
		return getAudioFileFormat(new Bitstream(in), len, null);
	}

	@Override
	public AudioFileFormat getAudioFileFormat(File file) throws UnsupportedAudioFileException, IOException {
		return getAudioFileFormat(new Bitstream(new FileInputStream(file)), file.length(), new LameSeekSupport(file));
	}

	private AudioInputStream getAudioInputStream(Bitstream bitstream, long len, LameSeekSupport seek_support) throws UnsupportedAudioFileException, IOException {
		AudioFormat format;
		Decoder decoder = new Decoder();
		final Header[] first_frame = new Header[1];
		try {
			first_frame[0] = bitstream.readFrame();
			if (first_frame[0] == null) {
				throw new UnsupportedAudioFileException("NOT valid MP3 stream!");
			}
			int channels = first_frame[0].mode() == Header.SINGLE_CHANNEL ? 1 : 2;
			int srate = first_frame[0].frequency();
			format = new AudioFormat(srate, 16, channels, true, BYTEORDER == ByteOrder.BIG_ENDIAN);
		} catch (Exception e) {
			throw new UnsupportedAudioFileException(e.getLocalizedMessage());
		}
		long length = (long) (first_frame[0].total_ms(len) / 1000f * first_frame[0].frequency());
		AudioByteBuffer abb = new AudioByteBuffer() {
			Header h = first_frame[0];

			@Override
			public void requestNewBuffer() throws IOException {
				try {
					while (true) {
						if (h == null) {
							h = bitstream.readFrame();
						}
						if (h == null) {
							break;
						}
						SampleBuffer output = (SampleBuffer) decoder.decodeFrame(h, bitstream);
						short[] s_buf = output.getBuffer();
						bitstream.closeFrame();
						h = null;
						if (output.getBufferLength() < 1) {
							continue;
						}
						ByteBuffer bb = ByteBuffer.allocate(output.getBufferLength() * 2).order(BYTEORDER);
						bb.asShortBuffer().put(s_buf, 0, output.getBufferLength());
						byte[] b = bb.array();
						offer(b);
						break;
					}
				} catch (Exception e) {
					close();
				}
			}
		};
		if (seek_support != null) {
			seek_support.setBitStream(bitstream);
		}
		AudioInputStream ais = new AudioInputStream(abb, format, length);
		FloatInputStream fis = new FloatInputStream(ais, format, length) {

			@Override
			public boolean supportsSeek() {
				return seek_support != null && seek_support.isEnabled();
			}

			@Override
			public void seek(long sample_position) throws IOException {
				abb.clear();
				seek_support.seek(sample_position);
			}
		};
		if (seek_support != null) {
			seek_support.setOutput(fis);
		}
		return fis;
	}

	@Override
	public AudioInputStream getAudioInputStream(Path path) throws UnsupportedAudioFileException, IOException {
		LameSeekSupport lss = new LameSeekSupport(path);
		Bitstream bit_stream = new Bitstream(Files.newByteChannel(path));
		lss.setBitStream(bit_stream);
		long file_len = Files.size(path);
		AudioFileFormat aff = getAudioFileFormat(bit_stream, file_len, lss);
		bit_stream.position(0);

		AudioByteBuffer abb = new AudioByteBuffer() {
			Header h = null;
			Decoder decoder = new Decoder();

			@Override
			public void requestNewBuffer() throws IOException {
				try {
					while (true) {
						h = bit_stream.readFrame();
						if (h == null) {
							break;
						}
						SampleBuffer output = (SampleBuffer) decoder.decodeFrame(h, bit_stream);
						short[] s_buf = output.getBuffer();
						bit_stream.closeFrame();
						if (output.getBufferLength() < 1) {
							continue;
						}
						ByteBuffer bb = ByteBuffer.allocate(output.getBufferLength() * 2).order(BYTEORDER);
						bb.asShortBuffer().put(s_buf, 0, output.getBufferLength());
						byte[] b = bb.array();
						offer(b);
						break;
					}
				} catch (Exception e) {
					close();
				}
			}
		};
		AudioInputStream ais = new AudioInputStream(abb, aff.getFormat(), lss.maxIndex());
		FloatInputStream fis = new FloatInputStream(ais, aff.getFormat(), lss.maxIndex()) {

			@Override
			public boolean supportsSeek() {
				return lss.isEnabled();
			}

			@Override
			public void seek(long sample_position) throws IOException {
				abb.clear();
				lss.seek(sample_position);
			}
		};
		lss.setOutput(fis);
		return fis;
	}


	@Override
	public AudioInputStream getAudioInputStream(InputStream stream) throws UnsupportedAudioFileException, IOException {
		return getAudioInputStream(new Bitstream(stream), -1, null);
	}

	@Override
	public AudioInputStream getAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException {
		URLConnection conn = url.openConnection();
		long len = conn.getContentLengthLong();
		InputStream in = url.openStream();
		return getAudioInputStream(new Bitstream(in), len, null);
	}

	@Override
	public AudioInputStream getAudioInputStream(File file) throws UnsupportedAudioFileException, IOException {
		return getAudioInputStream(new Bitstream(new FileInputStream(file)), file.length(), new LameSeekSupport(file));
	}

	@Override
	public AudioFileFormat getAudioFileFormat(Path path) throws UnsupportedAudioFileException, IOException {
		LameSeekSupport lss = new LameSeekSupport(path);
		return getAudioFileFormat(new Bitstream(Files.newByteChannel(path)), Files.size(path), lss);
	}
}
